package jmine.tec.web.servlet.cipher.impl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InvalidObjectException;
import java.io.ObjectStreamException;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import jmine.tec.utils.Tuple;
import jmine.tec.utils.UnmodifiableTuple;
import jmine.tec.utils.collection.IteratorEnumeration;
import jmine.tec.utils.cripto.cipher.CipherInputStream;
import jmine.tec.utils.cripto.key.GeradorChaveRSA;
import jmine.tec.utils.parsers.TokenizingReader;
import jmine.tec.web.servlet.FilterServletInputStream;
import jmine.tec.web.servlet.cipher.SecureCommunicationHandler;
import jmine.tec.web.servlet.delegate.HttpServletRequestWrapper;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Implementacao de {@link SecureCommunicationHandler} responsavel por fazer wrapper em volta do request e do response pelo metodo
 * {@link #wrapRequestResponse(HttpServletRequest, HttpServletResponse)}
 * 
 * @author takeshi
 */
public class SecureCommunicationHandlerImpl extends AbstractSecureCommunicationHandler implements SecureCommunicationHandler {

    /**
     * HTTP content type header name.
     */
    public static final String CONTENT_TYPE = "Content-type";

    /**
     * Part of HTTP content type header.
     */
    public static final String MULTIPART = "multipart/";

    private final Key decodeKey;

    private final Cipher cipher;

    private final int chunkSize;

    private final Key encodeKey;

    /**
     * C'tir
     * 
     * @param encodeKey a chave publica remota usada para encodar a saida do response
     * @param decodeKey a chave privada local usada para desencodar a entrada do request
     * @param chunkSize tamanho do bloco usado para cifrar o response
     * @param cipher {@link Cipher} a ser usado tanto para o encoding quanto para o decoding
     */
    public SecureCommunicationHandlerImpl(Key encodeKey, Key decodeKey, int chunkSize, Cipher cipher) {
        this.decodeKey = decodeKey;
        this.cipher = GeradorChaveRSA.copyCipher(cipher);
        this.chunkSize = chunkSize;
        this.encodeKey = encodeKey;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Tuple<HttpServletRequest, HttpServletResponse> wrapRequestResponse(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        HttpServletRequest cipherRequest;
        if (this.isMultipart(request)) {
            // we can use the raw input body
            cipherRequest = this.wrapRawRequest(request);
        } else {
            // sould be a get/post/put/delete that is not a multipart with a body.
            cipherRequest = this.parseParametersWrapRequest(request);
        }
        return UnmodifiableTuple.newTuple(cipherRequest, this.wrapResponse(response));
    }

    /**
     * Cria um {@link HttpServletRequest} que faz o parse do body quando um dos metodos para pegar os parametros do request for pedido
     * 
     * @param request {@link HttpServletRequest} a ser decorado
     * @return {@link HttpServletRequest}
     * @throws IOException e
     */
    private HttpServletRequest parseParametersWrapRequest(HttpServletRequest request) throws IOException {
        return new ParseBodyRequestWrapper(request, this.cipher, this.decodeKey);
    }

    /**
     * Faz o parse da query string, transformando em um mapa de String para array de string
     * 
     * @param queryString a query string
     * @return {@link Map}
     */
    public static Map<String, String[]> fromQueryString(String queryString) {
        if (queryString == null) {
            return new HashMap<String, String[]>();
        }
        try {
            TokenizingReader reader = new TokenizingReader(new StringReader(queryString), '&');
            HashMap<String, String[]> hashMap = new HashMap<String, String[]>();
            String token = reader.nextToken();
            while (token != null) {
                putParameter(token, hashMap);
                token = reader.nextToken();
            }
            return hashMap;
        } catch (IOException e) {
            throw new Error("StringReader threw IOException", e);
        }
    }

    /**
     * Faz o parse dos parametros a partir do {@link Reader} passado, adicionando ao mapa passado e devolvendo-o
     * 
     * @param reader {@link Reader}
     * @param originalMap {@link Map}
     * @return {@link Map}
     * @throws IOException e
     */
    public static Map<String, String[]> parseHttpParameters(Reader reader, Map<String, String[]> originalMap) throws IOException {
        TokenizingReader tokenizer = new TokenizingReader(reader, '&');
        String token = tokenizer.nextToken();
        while (token != null) {
            putParameter(token, originalMap);
            token = tokenizer.nextToken();
        }
        return originalMap;
    }

    /**
     * Adiciona um 'token' no formato key=chave ao mapa passado, alocando um novo array
     * 
     * @param token o token no formato
     * @param parameters mapa de paramtros
     */
    public static void putParameter(String token, Map<String, String[]> parameters) {
        String[] parts = token.split("=");
        try {
            String key = URLDecoder.decode(parts[0], "UTF-8");
            String value = parts.length > 1 ? URLDecoder.decode(parts[1], "UTF-8") : "";
            String[] arrayValue = parameters.get(key);
            if (arrayValue == null) {
                arrayValue = new String[]{ value };
            } else {
                arrayValue = addToArray(arrayValue, value);
            }
            parameters.put(key, arrayValue);
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("UTF-8 not supported", e);
        }
    }

    /**
     * Adiciona um valor a um array, alocando um array novo com capacidade maior, copiando o conteudo do array antigo, mantendo a ordem e
     * devolvendo o array passado
     * 
     * @param present array de {@link String}
     * @param value String
     * @return array de String
     */
    public static String[] addToArray(String[] present, String value) {
        String[] copy = new String[present.length + 1];
        System.arraycopy(present, 0, copy, 0, present.length);
        copy[present.length] = value;
        return copy;
    }

    /**
     * Faz um wrap do request as is, decodificando o body do request quando o metodo {@link HttpServletRequest#getInputStream()} ou
     * {@link HttpServletRequest#getReader()} for chamado. Chamadas sucessivas a algum dos metodos pode lancar alguma excecao, mas esse
     * comportamento fica a cargo do servidor de aplicacoes
     * 
     * @param request o {@link HttpServletRequest} a ser decorado
     * @return {@link HttpServletRequest}
     * @throws IOException se a chave passada for invalida
     */
    private HttpServletRequest wrapRawRequest(HttpServletRequest request) throws IOException {
        try {
            final InputStream input = new CipherInputStream(request.getInputStream(), this.cipher, this.decodeKey);
            return new HttpServletRequestWrapper(request) {
                @Override
                public ServletInputStream getInputStream() throws IOException {
                    return new FilterServletInputStream(input);
                }

                @Override
                public int getContentLength() {
                    return -1;
                }

            };
        } catch (InvalidKeyException e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * Decora um {@link HttpServletResponse}, codificando o body da resposta
     * 
     * @param response {@link HttpServletResponse}
     * @return {@link HttpServletResponse}
     */
    private HttpServletResponse wrapResponse(HttpServletResponse response) {
        return new CipherHttpServletResponse(response, this.cipher, this.encodeKey, getChunkSize());
    }

    /**
     * Verifica se o request eh um multipart
     * 
     * @param request {@link HttpServletRequest}
     * @return boolean
     */
    private boolean isMultipart(HttpServletRequest request) {
        String contentType = request.getHeader(CONTENT_TYPE);
        if (contentType == null) {
            return false;
        }
        if (contentType.startsWith(MULTIPART)) {
            return true;
        }
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean proceedRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
        return true;
    }

    /**
     * @return in order to serialize things
     * @throws ObjectStreamException e
     */
    protected Object writeReplace() throws ObjectStreamException {
        return new SerializedCommunicationSpec(this.encodeKey.getEncoded(), this.decodeKey.getEncoded(), getChunkSize(), getAlgorithm());
    }

    /**
     * @return int
     */
    protected int getChunkSize() {
        return this.chunkSize;
    }

    /**
     * @return String
     */
    protected String getAlgorithm() {
        return this.cipher.getAlgorithm();
    }

    /**
     * Decora um {@link HttpServletResponse}, decodificando os parametros do
     * 
     * @author takeshi
     */
    private static final class ParseBodyRequestWrapper extends HttpServletRequestWrapper {
        private Map<String, String[]> params;

        private IOException thrown;

        private final Cipher cipher;

        private final Key decodeKey;

        private final Log logger = LogFactory.getLog(this.getClass());

        /**
         * Constructor
         * 
         * @param request {@link HttpServletRequest}
         * @param ci {@link Cipher}
         * @param key the {@link Key} to decode
         */
        private ParseBodyRequestWrapper(HttpServletRequest request, Cipher ci, Key key) {
            super(request);
            this.cipher = GeradorChaveRSA.copyCipher(ci);
            this.decodeKey = key;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public int getContentLength() {
            return -1;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String getParameter(String name) {
            String[] value = this.getParameterMap().get(name);
            if (value == null) {
                return null;
            }
            return value[0];
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public synchronized Map<String, String[]> getParameterMap() {
            if (this.params == null) {
                try {
                    this.params = parseHttpParameters(this.getReader(), fromQueryString(this.getQueryString()));
                } catch (IOException e) {
                    this.logger.warn("IOException parsing post body", e);
                    this.thrown = e;
                }
            }
            return this.params;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public ServletInputStream getInputStream() throws IOException {
            if (this.thrown != null) {
                throw this.thrown;
            }
            try {
                return new FilterServletInputStream(new CipherInputStream(super.getInputStream(), this.cipher, this.decodeKey));
            } catch (InvalidKeyException e) {
                throw new IOException(e.getMessage());
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public BufferedReader getReader() throws IOException {
            String charset = this.getCharacterEncoding();
            if (charset == null) {
                charset = "ISO-8859-1";
            }
            return new BufferedReader(new InputStreamReader(this.getInputStream(), charset));
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Enumeration getParameterNames() {
            return new IteratorEnumeration<String>(this.getParameterMap().keySet());
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String[] getParameterValues(String name) {
            return this.getParameterMap().get(name);
        }
    }

    /**
     * Guarda a forma serializada do {@link SecureCommunicationHandlerImpl}
     * 
     * @author takeshi
     */
    private static class SerializedCommunicationSpec implements Serializable {
        private final byte[] encodeKey, decodeKey;

        private final int chunkSize;

        private final String cipherSpec;

        /**
         * C'tor
         * 
         * @param encodeKey a chave que deve ser usada para encoding
         * @param decodeKey a chave que deve ser usada para decoding
         * @param chunkSize o tamanho da janela de encriptacao
         * @param cipherSpec o cipher
         */
        public SerializedCommunicationSpec(byte[] encodeKey, byte[] decodeKey, int chunkSize, String cipherSpec) {
            this.encodeKey = encodeKey;
            this.decodeKey = decodeKey;
            this.chunkSize = chunkSize;
            this.cipherSpec = cipherSpec;
        }

        /**
         * Desserializa de volta o {@link SecureCommunicationHandlerImpl}
         * 
         * @return Object
         * @throws ObjectStreamException e
         */
        protected Object readResolve() throws ObjectStreamException {
            try {
                return new SecureCommunicationHandlerImpl(GeradorChaveRSA.leChavePublica(this.encodeKey), GeradorChaveRSA
                        .leChavePrivada(this.decodeKey), this.chunkSize, Cipher.getInstance(this.cipherSpec));
            } catch (InvalidKeySpecException e) {
                throw new InvalidObjectException(e.getMessage());
            } catch (NoSuchAlgorithmException e) {
                throw new InvalidObjectException(e.getMessage());
            } catch (NoSuchPaddingException e) {
                throw new InvalidObjectException(e.getMessage());
            }
        }
    }

}
